
/**
 * 动态路由功能
 * 作者：Yoyo
 * 介绍：自动根据\app\controller 控制器里面的 .js文件封装控制器 动态路由 文件请导出一个 实例对象
 * 时间：2022-10-17 17:01
 * 时间: 2023-02-04 14:00 更新深层的递归
 */

const Router = require("koa-router");
const Path = require("path");
const Fs = require("fs");
const router = new Router();
const controller = require('../config/controller');//引入控制器配置
const METHOD = ['get', 'put', 'post', 'patch', 'delete', 'del'];//支持的请求方法

// 最初的路径
const initPath = Path.join(__dirname, '../app/controller/');

eachController(initPath);//遍历控制器

/**
 * 递归遍历控制器文件夹
 * @param {String} path 路径
 */
function eachController(path) {
    let fsList = Fs.readdirSync(path, { withFileTypes: true });
    fsList.forEach(file => {
        if (!file.isFile()) {
            // 文件夹
            let NewPath = path + '/' + file.name;
            eachController(NewPath);//继续递归
        } else if (/.*\.js$/i.test(file.name)) {
            // js文件
            let fileName = Path.parse(file.name).name;//解析移除后缀
            trendsParse(path, fileName);
        }
    });
}


/**
 * 动态解析控制器路由
 * @param {String} path 路径
 * @param {String} fileName 文件名
 */
function trendsParse(path, fileName) {
    let filePath = Path.join(path, fileName);
    let classObject = require(filePath);
    let PropertyNames = Object.assign(classObject);//类的所有属性名和值 形成键对值(不包含私有)
    let methodNames = Object.getOwnPropertyNames(Object.getPrototypeOf(classObject)).filter(name => name != 'constructor');//返回类的所有方法(不包含 constructor )
    methodNames.forEach(funcName => {
        let Directory = path.replaceAll(initPath, '');
        let RouterRule = `${Directory}/${fileName}/${funcName}`;
        // 取得方法形参列表
        const fnText = classObject[funcName].toString();
        const paramNames = fnText.slice(fnText.indexOf('(') + 1, fnText.indexOf(')')).split(',').map(arg => arg.trim());

        let funcNum = classObject[funcName].length;//方法形参数量
        /* 如果参数大于1 就创建动态参数 */
        if (funcNum > 1) for (let i = 1; i < funcNum; i++) RouterRule += '/:param' + i + '?';

        // 拼接路由地址(包含多层)
        let routerCll = filePath.replaceAll(initPath, '');
        routerCll = routerCll.replaceAll('\\', '/');

        // 创建一个注入函数 方便传参数给ctx
        let introduction = (c, f) => {
            c = c ?? fileName;
            f = f ?? funcName;
            return async (ctx, next) => {
                let req = ctx.request;
                ylog.info(`Request: ${routerCll} Url: ${req.url} Method: ${req.method} body: ${getKeyValueString(req.body)} IP: ${ctx.fun.getClientIp()}`);
                ctx.__req = { controller: c, method: f, path: `/${routerCll}` };
                await next();
            };
        };

        //创建一个原型
        let FuncPrototype = async (ctx, next) => {
            let params = Object.values(ctx.params);//解析参数出来传送给方法
            if (params.length <= 0) {
                let queryList = ctx.request.query;//解析参数出来传送给方法 get的
                params = paramNames.slice(1).map(v => queryList[v]);
            }
            // ctx.body = await classObject[funcName](ctx, ...params);
            let results = classObject[funcName](ctx, ...params);
            if (isPromise(results)) {
                let resuls = await results;
                if (typeof resuls == 'undefined' && typeof ctx.body == 'undefined') {
                    ctx.body = '';
                } else if (resuls) {
                    ctx.body = resuls;
                }
            } else {
                if (typeof results == 'undefined' && typeof ctx.body == 'undefined') {
                    ctx.body = '';
                } else if (results) {
                    ctx.body = results;
                }
            }
            await next();
        };
        /* 注册请求方式 */
        let newMethod = setMethod(PropertyNames.__methods);
        /* 获取中间件 */
        let newMiddleware = setMiddleware(PropertyNames.__middleware);


        if (Array.isArray(newMethod)) {
            //注册 指定请求
            newMethod.forEach(type => {
                router[type](RouterRule, introduction(), ...newMiddleware, FuncPrototype);//只注册支持的请求方式
                if (funcName == 'index') router[type](`${Directory}/${fileName}`, introduction(fileName, 'index'), ...newMiddleware, FuncPrototype);//配置每个控制器的的入口
                if (controller.entrance === `/${routerCll}/${funcName}`) router[type]('/', introduction(fileName, funcName), ...newMiddleware, FuncPrototype);//配置入口路由
            });

        } else {
            //没有限制默认全部请求支持
            router.all(RouterRule, introduction(), ...newMiddleware, FuncPrototype);
            if (funcName == 'index') router.all(`${Directory}/${fileName}`, introduction(fileName, 'index'), ...newMiddleware, FuncPrototype);//配置每个控制器的的入口
            if (controller.entrance === `/${routerCll}/${funcName}`) router.all('/', introduction(fileName, funcName), ...newMiddleware, FuncPrototype);//配置入口路由
        }
    });
}

/**
 * 设置 Method 方式
 * @param {Array} Method
 * @returns {Array|false}
 */
function setMethod(Method) {
    if (typeof Method == 'undefined') return false;
    if (!Array.isArray(Method)) return false;
    if (Method.length <= 0) return false;
    let MethodFilter = Method.filter(type => METHOD.includes(String(type).toLowerCase())).map(type => String(type).toLowerCase());
    if (MethodFilter.length <= 0) return false;
    return MethodFilter;
}

/**
 * 设置 middleware 中间件
 * @param {Array} middleware
 * @returns {Array|false}
 */
function setMiddleware(middleware) {
    if (typeof middleware == 'undefined') return [];
    if (!Array.isArray(middleware)) return [];
    if (middleware.length <= 0) return [];
    let MiddlewareFilter = middleware.filter(type => typeof type == 'function');
    if (MiddlewareFilter.length <= 0) return [];
    return MiddlewareFilter;
}

/**
 * 判断是不是Promise对象
 * @param {Any} obj
 * @returns
 */
function isPromise(obj) {
    try {
        return obj instanceof Promise;
    } catch (error) {
        return false;
    }
}

/**
 * 对象转换为键值对的字符串表示形式
 * @param {*} obj 
 * @returns 
 */
function getKeyValueString(obj) {
    return Object.entries(obj)
        .map(([key, value]) => `${key}=>${value}`)
        .join(',');
}




module.exports = router;